#!/usr/bin/env python

# Copyright 2012 Google Inc. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#    http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Street Identity API."""

import base64
import json
import logging
import string
import time
from google.appengine.api import app_identity
from base import BaseHandler
import common
from lib import authz, scope_manager
from lib import utils
from lib import scope_utils
from lib import http
from lib.accept_content import accept_content
from lib.loggable import loggable
import model

__author__ = 'Maciej Machulak'
__maintainer__ = 'Maciej Machulak'
__email__ = 'mmachulak@google.com'

__copyright__ = 'Copyright 2012, Google Inc.'
__license__ = 'Apache License 2.0'
__version__ = '0.2'
__status__ = 'Prototype'

_SCOPE_PREFIX = 'https://www.googleapis.com/auth/'
_STREETIDENTITY_SCOPES_READ = [
  _SCOPE_PREFIX + 'streetidentity.read',
  _SCOPE_PREFIX + 'verifiedage.read',
  _SCOPE_PREFIX + 'verifiedgender.read']
_STREETIDENTITY_SCOPES_WRITE = [
  _SCOPE_PREFIX + 'streetidentity.write',
  _SCOPE_PREFIX + 'verifiedage.write',
  _SCOPE_PREFIX + 'verifiedgender.write']
_STREETIDENTITY_SCOPES_ALL = []
_STREETIDENTITY_SCOPES_ALL.extend(_STREETIDENTITY_SCOPES_READ)
_STREETIDENTITY_SCOPES_ALL.extend(_STREETIDENTITY_SCOPES_WRITE)

_UNPREFIXED_STREETIDENTITY_SCOPES_READ = [
  'streetidentity.read',
  'verifiedage.read',
  'verifiedgender.read']

_UNPREFIXED_STREETIDENTITY_SCOPES_WRITE = [
  'streetidentity.write',
  'verifiedage.write',
  'verifiedgender.write']

_UNPREFIXED_STREETIDENTITY_SCOPES_ALL = []
_UNPREFIXED_STREETIDENTITY_SCOPES_ALL.\
extend(_UNPREFIXED_STREETIDENTITY_SCOPES_READ)
_UNPREFIXED_STREETIDENTITY_SCOPES_ALL.\
extend(_UNPREFIXED_STREETIDENTITY_SCOPES_WRITE)

_SIG_ALG='RS256'

class DiscoveryHandler(BaseHandler):
  """Handler to provide info to RPs about registered attribute providers."""

  @authz.api_authz_required(scopes=_STREETIDENTITY_SCOPES_ALL)
  @loggable('discovery handler')
  def get(self, *args):

    user = self.user

    scope = self.request.get(common.SI_SCOPE, None)
    logging.debug('requested info about: %s', scope)
    requested_scopes = []
    if scope:
      logging.debug('obtain scope information from request')
      requested_scopes = [scope]
    if scope and scope.find(' ') >= 0:
      logging.debug('request for multiple scopes')
      requested_scopes = scope.split(' ')
    user_ap_map = {}
    if not requested_scopes:
      logging.debug('using all street identity scopes')
      requested_scopes = _STREETIDENTITY_SCOPES_READ
    logging.debug('showing attributes with types: %s', requested_scopes)

    authorized_scopes = scope_utils.filter_scopes(requested_scopes, self.scopes)

    if not authorized_scopes:
      error_response = {'error': "invalid_scope",
                        'error_description': 'bad_request' }
      self.response.set_status(403)
      self.response.headers[http.Header.content_type] = http.ContentType.json_utf8
      self.response.out.write(json.dumps(error_response))
      return

    for single_scope in authorized_scopes:
      logging.debug('retrieving APs for scope %s', single_scope)
      user_ap_list = model.AttributeProvider.all()\
                                        .filter('user = ', user)\
                                        .filter('scopes = ', single_scope)
      logging.debug('number of APs retrieved: %d', user_ap_list.count())
      logging.debug('building a list of client_ids')
      ap_client_id_list = []
      for single_ap in user_ap_list:
        logging.debug('adding client_id: %s', single_ap.application.client_id)
        ap_client_id_list.append(single_ap.application.client_id)
      logging.debug('adding client_id map for scope: %s', single_scope)
      user_ap_map[single_scope] = ap_client_id_list

    logging.debug('returning a list of attribute providers for user %s ', user.user_id)

    self.response.set_status(200)
    self.response.headers[http.Header.content_type] = http.ContentType.json_utf8
    self.response.out.write(json.dumps(user_ap_map))
    return


class TokenHandler(BaseHandler):
  """Issues tokens for accessing Attribute Providers."""

  @authz.api_authz_required(scopes=_STREETIDENTITY_SCOPES_ALL)
  @accept_content('application/x-www-form-urlencoded')
  @loggable('issue tokens to access attribute providers')
  def post(self):
    """Handles POST requests and issues tokens to RPs."""

    user = self.user
    application = self.application

    client_id = self.request.get('client_id')

    logging.debug(
      'received request for token for AP with client_id: %s', client_id)

    # Filter street identity READ scopes
    si_scopes = scope_utils.filter_scopes(self.scopes, _STREETIDENTITY_SCOPES_READ)

    # Getting attribute provider from DB
    attribute_provider = self._get_ap_for_user(user, client_id)

    if attribute_provider is None:
      logging.debug('no attribute provider registered for this user')
      self.response.set_status(403)
      return

    # Narrow down the scopes for this attribute provider
    authorized_scopes = []
    ap_scopes = attribute_provider.scopes
    read_scopes = scope_manager.get_dependent_scopes(ap_scopes)
    for scope in read_scopes:
      if scope in si_scopes:
        authorized_scopes.append(scope)

    if len(authorized_scopes) is None:
      logging.debug('client not authorized to access this attribute provider')
      self.response.set_status(403)
      return

    logging.debug(
      'successfully retrieved attribute provider - generating token')

    # Generate the token
    now = time.time()
    now_before = now
    logging.debug('issued at: %s', now_before)
    now_after = now + 60 * 15 #  15 minutes
    logging.debug('expires at: %s', now_after)
    application_id = app_identity.get_service_account_name()
    logging.debug('application id: %s', application_id)

    token = self._buildjwt(
      issuer=application_id,
      audience=attribute_provider.application.client_id,
      clientId=application.client_id,
      userId=user.user_id,
      scope=''.join(si_scopes),
      issued_at=now_before,
      expires=now_after)
    logging.debug('token: %s', token)  #  TODO(mmachulak) remove token logging!

    # Store the token within the datastore
    authorization = model.Authorization(
      application=application,
      attribute_provider=attribute_provider,
      iat=int(now_before),
      exp=int(now_after),
      scopes = si_scopes,
      token_hash = utils.get_token_hash(token)
    )
    authorization.put()
    logging.debug('storing authorization within the database')

    # Prepare the response
    response = {'token': token,
                'expires_in': int(now_after - now_before),
                'scope' : string.join(si_scopes)}

    self.response.set_status(200)
    self.response.headers[http.Header.content_type] =\
    'application/json; charset=utf-8'
    self.response.out.write(json.dumps(response))
    return

  def _get_ap_for_user(self, user, client_id):

    app = model.Application.all().filter('client_id = ', client_id).get()
    if not app:
      return None

    return model.AttributeProvider.all()\
    .filter('application = ', app)\
    .filter('user = ', user)\
    .get()

  def _buildjwt(self, issuer, audience, clientId, userId, scope, issued_at,
                expires):
    logging.debug('building signed JWT')
    logging.debug('issuer: %s', issuer)
    logging.debug('audience: %s', audience)
    logging.debug('clientId: %s', clientId)
    logging.debug('userId: %s', userId)
    logging.debug('scope: %s', scope)
    logging.debug('issued_at: %s', str(issued_at))
    logging.debug('expires: %s', str(expires))
    jsonpayload = ('{\"issuer\":\"'
                   + issuer
                   + '\"audience\":\"'
                   + audience
                   + '\",\"client_id\":\"'
                   + clientId
                   + '\",\"user_id\":\"'
                   + userId
                   + '\",\"scope\":\"'
                   + scope
                   + '\",\"issued_at\":'
                   + str(issued_at)
                   + ',\"expires_at\":'
                   + str(expires)
                   + '}'
      )

    logging.debug(jsonpayload)

    header = ('{\"alg\":\"'
              + _SIG_ALG
              + '\",\"kid\":\"'
              + ''
              + '\"}'
      )

    logging.debug(header)

    basesignaturestring = (base64.urlsafe_b64encode(header)
                           + '.'
                           + base64.urlsafe_b64encode(jsonpayload)
      )
    result = app_identity.sign_blob(basesignaturestring)
    keyid = result[0]
    sig = result[1]
    signedjsontoken = (basesignaturestring
                       + '.'
                       + base64.urlsafe_b64encode(sig))
    return signedjsontoken

class TokenInfoHandler(BaseHandler):
  """Handler for validating tokens for Attribute Providers."""

  @authz.api_authz_required(scopes=_STREETIDENTITY_SCOPES_ALL)
  @accept_content('application/x-www-form-urlencoded')
  @loggable('attribute provider token info')
  def post(self, *args):
    """Handles requests from APs and validates tokens."""

    try:

      token = self.request.get('token', None)

      if not token:

        error_response = {'error': "required",
                          'error_description': 'access_token required' }
        raise TokenInfoError(error_response)

      token_info_response = self._verify_encrypted_token(token)

      logging.debug('successfully obtained token info')
      self.response.set_status(200)
      self.response.headers[http.Header.content_type] = http.ContentType.json_utf8
      self.response.out.write(json.dumps(token_info_response))
      return

    except TokenInfoError as ex:

      logging.error('could not verify token')
      error_response = ex.args[0]
      self.response.set_status(400)
      self.response.headers[http.Header.content_type] = http.ContentType.json_utf8
      self.response.out.write(json.dumps(error_response))

  def _verify_encrypted_token(self, token):

    token_hash = utils.get_token_hash(token)

    authorization = model.Authorization.all()\
    .filter('token_hash = ', token_hash).get()

    if authorization:
      token_info_response = {'issuer':
                               app_identity.get_service_account_name(),
                             'audience':
                               authorization.attribute_provider.application.client_id,
                             'client_id': authorization.application.client_id,
                             'user_id':
                               authorization.attribute_provider.user.user_id,
                             'scope': ''.join(authorization.scopes),
                             'issued_at': authorization.iat,
                             'expires_at': authorization.exp}
      return token_info_response

    error_response = {'error': "invalid_token",
                      'error_description': 'bad_request' }
    raise TokenInfoError(error_response)

class TokenInfoError(Exception):
  """Token Info Error raised when token could not be verified."""

  pass
